/**
 * PHOSIDE: PHosphorylation Site IDentification Engine.
 * Copyright 2009 Digital Biology Lab, University of Missouri.
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version. <p/> This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the License for more
 * details.
 */

package phoside.classify;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;

import java.util.ArrayList;
import java.util.List;

import phoside.PhosideInit;

import phoside.data.feature.model.Instance;
import phoside.data.feature.extaction.FeatureExtractor;

import phoside.util.IOUtil;
import phoside.util.StaticFinalValues;

/**
 *
 * @author gjj
 */
public class SVMLightBinaryClassifier implements BinaryClassifier, Serializable {
    private static final long serialVersionUID = -8163571852149370448L;

    protected final List<FeatureExtractor> extractors;

    protected final String svmLightOption;
    protected final FeatureNormalizer featureNormalizer;

    protected List<String> model;

    private static final int sbBuf = 1000;

    public SVMLightBinaryClassifier(final String svmLightOption,
                                    final FeatureNormalizer featureNormalizer) {
        this(svmLightOption, featureNormalizer, null);
    }

    public SVMLightBinaryClassifier(final String svmLightOption,
                                    final FeatureNormalizer featureNormalizer,
                                    final List<FeatureExtractor> extractors) {
        if (svmLightOption==null) {
            throw new NullPointerException();
        }
        
        this.extractors = extractors;
        this.svmLightOption = svmLightOption;
        this.featureNormalizer = featureNormalizer;
        

    }

    /**
     *
     * @param instances
     * @param positive
     * @return
     */
    public boolean train(List<Instance> instances, List<Double> label) {        
        long currTime = System.currentTimeMillis();
        String tmpDirData = PhosideInit.tmpDir.getAbsolutePath()+File.separator
                +currTime+".data.tmp";
        String tmpDirModel = PhosideInit.tmpDir.getAbsolutePath()+File.separator
                +currTime+".model.tmp";

        if (instances==null || label==null) {
            throw new NullPointerException();
        }

        if (instances.size() != label.size()) {
            throw new IllegalArgumentException("Each instance should have exact one lable.");
        }

        boolean ret = false;

        if (prepareSVMLightTrainingData(tmpDirData, instances, label)) {
            if (runSVMLightTrain(tmpDirData, tmpDirModel)) {
                if (readModel(tmpDirModel)) {
                    ret = true;
                }
            }
        }

        try {
            IOUtil.deleteFile(tmpDirData);
            IOUtil.deleteFile(tmpDirModel);
        } catch(Exception ex) {
            ex.printStackTrace();
        }

        return ret;
    }

    public List<Double> classify(final List<Instance> instances) {
        long currTime = System.currentTimeMillis();
        String tmpDirData = PhosideInit.tmpDir.getAbsolutePath()+File.separator
                +currTime+".data.tmp";
        String tmpDirModel = PhosideInit.tmpDir.getAbsolutePath()+File.separator
                +currTime+".model.tmp";
        String tmpDirResult = PhosideInit.tmpDir.getAbsolutePath()+File.separator
                +currTime+".res.tmp";

        if (instances==null) {
            throw new NullPointerException();
        }

        if (model==null) {
            throw new IllegalStateException("Train first!");
        }

        List<Double> ret = null;
        List<Boolean> succ = prepareSVMLightTestingData(tmpDirData, instances);
        if (succ!=null) {
            if (writeModel(tmpDirModel)) {
                if (runSVMLightClassify(tmpDirData,tmpDirModel,tmpDirResult)) {
                    ret = parseResult(tmpDirResult);
                    int n = succ.size();
                    for (int i=0; i<n; i++) {
                        if (!succ.get(i)) {
                            ret.set(i, Double.NEGATIVE_INFINITY);
                        }
                    }
                }
            }
        }

        try {
            IOUtil.deleteFile(tmpDirData);
            IOUtil.deleteFile(tmpDirModel);
            IOUtil.deleteFile(tmpDirResult);
        } catch(Exception ex) {
            ex.printStackTrace();
        }

        return ret;
    }

    protected boolean prepareSVMLightTrainingData(final String tmpDirData,
            final List<Instance> instances, final List<Double> positive) {
        int n = instances.size();
        List<List<Double>> features = new ArrayList<List<Double>>(n);
        List<Double> positiveCopy = new ArrayList(n);
        for (int i=0; i<n; i++) {
            List<Double> fs = extractFeature(instances.get(i));
            
            if (fs==null || fs.isEmpty()) {
                //System.out.println();
            } else {
                positiveCopy.add(positive.get(i));
                features.add(fs);
            }
        }

        if (featureNormalizer!=null) {
            featureNormalizer.trainParameter(features);
        }

        n = features.size();

        try {
            Writer fout = new FileWriter(tmpDirData);
            BufferedWriter out = new BufferedWriter(fout);

            StringBuilder str = new StringBuilder(sbBuf);
            
            for (int i=0; i<n; i++) {
                List<Double> fs = features.get(i);

                if (featureNormalizer!=null) {
                    featureNormalizer.normalize(fs);
                }

                str.setLength(0); // clear
                str.append(positiveCopy.get(i).intValue());

                int nf = fs.size();
                for (int itf=0; itf<nf; itf++) {
                    double f = fs.get(itf);
                    str.append(" "+(itf+1)+":"+f);
                }
                out.write(str.toString());
                out.newLine();
            }

            out.close();
            fout.close();
            return true;
        } catch (IOException ex) {
            ex.printStackTrace();
            return false;
        }
    }

    protected boolean runSVMLightTrain(final String tmpDirData,
            final String tmpDirModel) {
        final String dirSVMLightTrain = PhosideInit.globalProps.getProperty(
                StaticFinalValues.SVMTRAINDIR);

        String cmd = "\""+dirSVMLightTrain+"\""+
                     " "+svmLightOption+" "+
                     " \""+tmpDirData+"\""+
                     " \""+tmpDirModel+"\"";
//        ProcessBuilder pb = new ProcessBuilder(dirSVMLightTrain, svmLightOption,
//                tmpDirData, tmpDirModel);
        //pb.redirectErrorStream(true);

        try {
            Process process = Runtime.getRuntime().exec(cmd);
//            Process process = pb.start();
            phoside.util.ExecUtil.redirect(process, System.err, System.out);
            int retcode = process.waitFor();
            return retcode==0;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    
    protected List<Boolean> prepareSVMLightTestingData(final String tmpDirData,
            final List<Instance> instances) {
        
        int n = instances.size();
        List<Boolean> succ = new ArrayList();

        try {
            Writer fout = new FileWriter(tmpDirData);
            BufferedWriter out = new BufferedWriter(fout);

            StringBuilder str = new StringBuilder(sbBuf);

            for (int i=0; i<n; i++) {
                List<Double> fs = extractFeature(instances.get(i));

                if (fs==null || fs.isEmpty()) {
                    out.write("0");
                    out.newLine();
                    succ.add(Boolean.FALSE);
                    continue;
                    //return false;
                }
                
                if (featureNormalizer!=null) {
                    featureNormalizer.normalize(fs);
                }
                
                str.setLength(0);
                str.append("0");

                int nf = fs.size();
                for (int itf=0; itf<nf; itf++) {
                    double f = fs.get(itf);
                    str.append(" "+(itf+1)+":"+f);
                }
                out.write(str.toString());
                out.newLine();
                succ.add(Boolean.TRUE);
            }

            out.close();
            fout.close();
            return succ;
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    private List<Double> extractFeature(Instance ins) {
        List<Double> res = ins.getFeatures();

        if (extractors!=null) {
            for (FeatureExtractor extractor : extractors) {
                List<Double> fs;
                try {
                    fs = extractor.extract(ins, false);
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }

                if (fs==null) {
                    return null;
                }
                
                res.addAll(fs);
            }
        } 

        return res;
    }

    protected boolean runSVMLightClassify(final String tmpDirData,
            final String tmpDirModel, final String tmpDirResult) {
        final String dirSVMLightClassify = PhosideInit.globalProps.getProperty(
                StaticFinalValues.SVMCLASSIFYDIR);
        String cmd = "\""+dirSVMLightClassify+"\""+
                     " \""+tmpDirData+"\""+
                     " \""+tmpDirModel+"\""+
                     " \""+tmpDirResult+"\"";
//        ProcessBuilder pb = new ProcessBuilder(dirSVMLightClassify, tmpDirData,
//                tmpDirModel, tmpDirResult);
//        pb.redirectErrorStream(true);

        try {
            Process process = Runtime.getRuntime().exec(cmd);
            phoside.util.ExecUtil.redirect(process, System.err, System.out);
//            Process process = pb.start();
            int retcode = process.waitFor();
            return retcode==0;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }

    protected List<Double> parseResult(final String tmpDirResult) {
        try {
            return IOUtil.readDoubleListAscii(tmpDirResult);
        } catch(IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    protected boolean readModel(final String tmpDirModel) {
        try {
            model = IOUtil.readStringListAscii(tmpDirModel);
            return true;
        } catch (IOException ex) {
            ex.printStackTrace();
            return false;
        }
    }

    protected boolean writeModel(final String tmpDirModel) {
        try {
            IOUtil.writeCollectionAscii(model, tmpDirModel);
            return true;
        } catch (IOException ex) {
            ex.printStackTrace();
            return false;
        }
    }

    
}
